/*
 * Created on 1 Nov 2006
 * Created by Paul Gardner
 * Copyright (C) Azureus Software, Inc, All Rights Reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 */

package com.aelitis.azureus.core.stats;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

import org.gudy.azureus2.core3.util.AEDiagnostics;
import org.gudy.azureus2.core3.util.AEDiagnosticsEvidenceGenerator;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.IndentWriter;
import org.gudy.azureus2.core3.util.Timer;
import org.gudy.azureus2.core3.util.TimerEvent;
import org.gudy.azureus2.core3.util.TimerEventPerformer;

import com.aelitis.azureus.core.util.CopyOnWriteList;
import com.aelitis.azureus.core.util.average.Average;
import com.aelitis.azureus.core.util.average.AverageFactory;

public class AzureusCoreStats {
    public static final int AVERAGE_PERIOD = 1000;

    public static final String ST_ALL = ".*";

    // DISK

    public static final String ST_DISK = "disk.*";
    public static final String ST_DISK_READ_QUEUE_LENGTH = "disk.read.queue.length";
    public static final String ST_DISK_READ_QUEUE_BYTES = "disk.read.queue.bytes";
    public static final String ST_DISK_READ_REQUEST_COUNT = "disk.read.request.count";
    public static final String ST_DISK_READ_REQUEST_SINGLE = "disk.read.request.single";
    public static final String ST_DISK_READ_REQUEST_MULTIPLE = "disk.read.request.multiple";
    public static final String ST_DISK_READ_REQUEST_BLOCKS = "disk.read.request.blocks";
    public static final String ST_DISK_READ_BYTES_TOTAL = "disk.read.bytes.total";
    public static final String ST_DISK_READ_BYTES_SINGLE = "disk.read.bytes.single";
    public static final String ST_DISK_READ_BYTES_MULTIPLE = "disk.read.bytes.multiple";
    public static final String ST_DISK_READ_IO_TIME = "disk.read.io.time";
    public static final String ST_DISK_READ_IO_COUNT = "disk.read.io.count";

    public static final String ST_DISK_WRITE_QUEUE_LENGTH = "disk.write.queue.length";
    public static final String ST_DISK_WRITE_QUEUE_BYTES = "disk.write.queue.bytes";
    public static final String ST_DISK_WRITE_REQUEST_COUNT = "disk.write.request.count";
    public static final String ST_DISK_WRITE_REQUEST_BLOCKS = "disk.write.request.blocks";
    public static final String ST_DISK_WRITE_BYTES_TOTAL = "disk.write.bytes.total";
    public static final String ST_DISK_WRITE_BYTES_SINGLE = "disk.write.bytes.single";
    public static final String ST_DISK_WRITE_BYTES_MULTIPLE = "disk.write.bytes.multiple";
    public static final String ST_DISK_WRITE_IO_TIME = "disk.write.io.time";
    public static final String ST_DISK_WRITE_IO_COUNT = "disk.write.io.count";

    // NETWORK

    public static final String ST_NET_WRITE_CONTROL_WAIT_COUNT = "net.write.control.wait.count";
    public static final String ST_NET_WRITE_CONTROL_NP_COUNT = "net.write.control.np.count";
    public static final String ST_NET_WRITE_CONTROL_P_COUNT = "net.write.control.p.count";
    public static final String ST_NET_WRITE_CONTROL_ENTITY_COUNT = "net.write.control.entity.count";
    public static final String ST_NET_WRITE_CONTROL_CON_COUNT = "net.write.control.con.count";
    public static final String ST_NET_WRITE_CONTROL_READY_CON_COUNT = "net.write.control.ready.con.count";
    public static final String ST_NET_WRITE_CONTROL_READY_BYTE_COUNT = "net.write.control.ready.byte.count";

    public static final String ST_NET_READ_CONTROL_LOOP_COUNT = "net.read.control.loop.count";
    public static final String ST_NET_READ_CONTROL_NP_COUNT = "net.read.control.np.count";
    public static final String ST_NET_READ_CONTROL_P_COUNT = "net.read.control.p.count";
    public static final String ST_NET_READ_CONTROL_WAIT_COUNT = "net.read.control.wait.count";
    public static final String ST_NET_READ_CONTROL_ENTITY_COUNT = "net.read.control.entity.count";
    public static final String ST_NET_READ_CONTROL_CON_COUNT = "net.read.control.con.count";
    public static final String ST_NET_READ_CONTROL_READY_CON_COUNT = "net.read.control.ready.con.count";

    // TCP

    public static final String ST_NET_TCP_OUT_CONNECT_QUEUE_LENGTH = "net.tcp.outbound.connect.queue.length";
    public static final String ST_NET_TCP_OUT_PENDING_QUEUE_LENGTH = "net.tcp.outbound.pending.queue.length";
    public static final String ST_NET_TCP_OUT_CANCEL_QUEUE_LENGTH = "net.tcp.outbound.cancel.queue.length";
    public static final String ST_NET_TCP_OUT_CLOSE_QUEUE_LENGTH = "net.tcp.outbound.close.queue.length";

    public static final String ST_NET_TCP_SELECT_WRITE_COUNT = "net.tcp.select.write.count";
    public static final String ST_NET_TCP_SELECT_READ_COUNT = "net.tcp.select.read.count";

    // HTTP

    public static final String ST_NET_HTTP_IN_REQUEST_COUNT = "net.http.inbound.request.count";
    public static final String ST_NET_HTTP_IN_REQUEST_OK_COUNT = "net.http.inbound.request.ok.count";
    public static final String ST_NET_HTTP_IN_REQUEST_INVALID_COUNT = "net.http.inbound.request.invalid.count";
    public static final String ST_NET_HTTP_IN_REQUEST_WEBSEED_COUNT = "net.http.inbound.request.webseed.count";
    public static final String ST_NET_HTTP_IN_REQUEST_GETRIGHT_COUNT = "net.http.inbound.request.getright.count";

    // Peer Control

    public static final String ST_PEER_CONTROL_SCHEDULE_COUNT = "peer.control.schedule.count";
    public static final String ST_PEER_CONTROL_LOOP_COUNT = "peer.control.loop.count";
    public static final String ST_PEER_CONTROL_YIELD_COUNT = "peer.control.yield.count";
    public static final String ST_PEER_CONTROL_WAIT_COUNT = "peer.control.wait.count";
    public static final String ST_PEER_CONTROL_WAIT_TIME = "peer.control.wait.time";

    // Peer Manager

    public static final String ST_PEER_MANAGER_COUNT = "peer.manager.count";
    public static final String ST_PEER_MANAGER_PEER_COUNT = "peer.manager.peer.count";
    public static final String ST_PEER_MANAGER_PEER_SNUBBED_COUNT = "peer.manager.peer.snubbed.count";
    public static final String ST_PEER_MANAGER_PEER_STALLED_DISK_COUNT = "peer.manager.peer.stalled.disk.count";

    // Tracker

    public static final String ST_TRACKER_READ_BYTES = "tracker.read.bytes.total";
    public static final String ST_TRACKER_WRITE_BYTES = "tracker.write.bytes.total";
    public static final String ST_TRACKER_ANNOUNCE_COUNT = "tracker.announce.count";
    public static final String ST_TRACKER_ANNOUNCE_TIME = "tracker.announce.time";
    public static final String ST_TRACKER_SCRAPE_COUNT = "tracker.scrape.count";
    public static final String ST_TRACKER_SCRAPE_TIME = "tracker.scrape.time";

    // xfer (persistent)

    public static final String ST_XFER_UPLOADED_PROTOCOL_BYTES = "xfer.upload.protocol.bytes.total";
    public static final String ST_XFER_UPLOADED_DATA_BYTES = "xfer.upload.data.bytes.total";
    public static final String ST_XFER_DOWNLOADED_PROTOCOL_BYTES = "xfer.download.protocol.bytes.total";
    public static final String ST_XFER_DOWNLOADED_DATA_BYTES = "xfer.download.data.bytes.total";

    public static final String POINT = "Point";
    public static final String CUMULATIVE = "Cumulative";

    private static final List stats_names = new ArrayList();
    private static final Map stats_types = new HashMap();

    private static final String[][] _ST_ALL = {

    { ST_DISK_READ_QUEUE_LENGTH, POINT }, { ST_DISK_READ_QUEUE_BYTES, POINT }, { ST_DISK_READ_REQUEST_COUNT, CUMULATIVE },
            { ST_DISK_READ_REQUEST_SINGLE, CUMULATIVE }, { ST_DISK_READ_REQUEST_MULTIPLE, CUMULATIVE },
            { ST_DISK_READ_REQUEST_BLOCKS, CUMULATIVE }, { ST_DISK_READ_BYTES_TOTAL, CUMULATIVE }, { ST_DISK_READ_BYTES_SINGLE, CUMULATIVE },
            { ST_DISK_READ_BYTES_MULTIPLE, CUMULATIVE }, { ST_DISK_READ_IO_TIME, CUMULATIVE }, { ST_DISK_READ_IO_COUNT, CUMULATIVE },

            { ST_DISK_WRITE_QUEUE_LENGTH, POINT }, { ST_DISK_WRITE_QUEUE_BYTES, POINT }, { ST_DISK_WRITE_REQUEST_COUNT, CUMULATIVE },
            { ST_DISK_WRITE_REQUEST_BLOCKS, CUMULATIVE }, { ST_DISK_WRITE_BYTES_TOTAL, CUMULATIVE }, { ST_DISK_WRITE_BYTES_SINGLE, CUMULATIVE },
            { ST_DISK_WRITE_BYTES_MULTIPLE, CUMULATIVE }, { ST_DISK_WRITE_IO_TIME, CUMULATIVE }, { ST_DISK_WRITE_IO_COUNT, CUMULATIVE },

            { ST_NET_WRITE_CONTROL_WAIT_COUNT, CUMULATIVE }, { ST_NET_WRITE_CONTROL_P_COUNT, CUMULATIVE },
            { ST_NET_WRITE_CONTROL_NP_COUNT, CUMULATIVE }, { ST_NET_WRITE_CONTROL_ENTITY_COUNT, POINT }, { ST_NET_WRITE_CONTROL_CON_COUNT, POINT },
            { ST_NET_WRITE_CONTROL_READY_CON_COUNT, POINT }, { ST_NET_WRITE_CONTROL_READY_BYTE_COUNT, POINT },

            { ST_NET_READ_CONTROL_LOOP_COUNT, CUMULATIVE }, { ST_NET_READ_CONTROL_P_COUNT, CUMULATIVE },
            { ST_NET_READ_CONTROL_NP_COUNT, CUMULATIVE }, { ST_NET_READ_CONTROL_WAIT_COUNT, CUMULATIVE },
            { ST_NET_READ_CONTROL_ENTITY_COUNT, POINT }, { ST_NET_READ_CONTROL_CON_COUNT, POINT }, { ST_NET_READ_CONTROL_READY_CON_COUNT, POINT },

            { ST_NET_TCP_OUT_CONNECT_QUEUE_LENGTH, POINT }, { ST_NET_TCP_OUT_PENDING_QUEUE_LENGTH, POINT },
            { ST_NET_TCP_OUT_CANCEL_QUEUE_LENGTH, POINT }, { ST_NET_TCP_OUT_CLOSE_QUEUE_LENGTH, POINT },

            { ST_NET_TCP_SELECT_WRITE_COUNT, CUMULATIVE }, { ST_NET_TCP_SELECT_READ_COUNT, CUMULATIVE },

            { ST_NET_HTTP_IN_REQUEST_COUNT, CUMULATIVE }, { ST_NET_HTTP_IN_REQUEST_OK_COUNT, CUMULATIVE },
            { ST_NET_HTTP_IN_REQUEST_INVALID_COUNT, CUMULATIVE }, { ST_NET_HTTP_IN_REQUEST_WEBSEED_COUNT, CUMULATIVE },
            { ST_NET_HTTP_IN_REQUEST_GETRIGHT_COUNT, CUMULATIVE },

            { ST_PEER_CONTROL_SCHEDULE_COUNT, CUMULATIVE }, { ST_PEER_CONTROL_LOOP_COUNT, CUMULATIVE },
            { ST_PEER_CONTROL_YIELD_COUNT, CUMULATIVE }, { ST_PEER_CONTROL_WAIT_COUNT, CUMULATIVE }, { ST_PEER_CONTROL_WAIT_TIME, CUMULATIVE },

            { ST_PEER_MANAGER_COUNT, POINT }, { ST_PEER_MANAGER_PEER_COUNT, POINT }, { ST_PEER_MANAGER_PEER_SNUBBED_COUNT, POINT },
            { ST_PEER_MANAGER_PEER_STALLED_DISK_COUNT, POINT },

            { ST_TRACKER_READ_BYTES, CUMULATIVE }, { ST_TRACKER_WRITE_BYTES, CUMULATIVE }, { ST_TRACKER_ANNOUNCE_COUNT, CUMULATIVE },
            { ST_TRACKER_ANNOUNCE_TIME, CUMULATIVE }, { ST_TRACKER_SCRAPE_COUNT, CUMULATIVE }, { ST_TRACKER_SCRAPE_TIME, CUMULATIVE },

            { ST_XFER_UPLOADED_PROTOCOL_BYTES, CUMULATIVE }, { ST_XFER_UPLOADED_DATA_BYTES, CUMULATIVE },
            { ST_XFER_DOWNLOADED_PROTOCOL_BYTES, CUMULATIVE }, { ST_XFER_DOWNLOADED_DATA_BYTES, CUMULATIVE }, };

    static {

        addStatsDefinitions(_ST_ALL);

        AEDiagnostics.addEvidenceGenerator(new AEDiagnosticsEvidenceGenerator() {
            public void generate(IndentWriter writer) {
                writer.println("Stats");

                boolean turn_on_averages = !getEnableAverages();

                try {
                    writer.indent();

                    if (turn_on_averages) {

                        setEnableAverages(true);

                        try {
                            Thread.sleep(AVERAGE_PERIOD * 5);

                        } catch (Throwable e) {
                        }
                    }

                    Set types = new HashSet();

                    types.add(ST_ALL);

                    Map reply = getStats(types);

                    Iterator it = reply.entrySet().iterator();

                    List lines = new ArrayList();

                    while (it.hasNext()) {

                        Map.Entry entry = (Map.Entry) it.next();

                        lines.add(entry.getKey() + " -> " + entry.getValue());
                    }

                    Collections.sort(lines);

                    for (int i = 0; i < lines.size(); i++) {

                        writer.println((String) lines.get(i));
                    }

                } finally {

                    if (turn_on_averages) {

                        setEnableAverages(false);
                    }

                    writer.exdent();
                }
            }
        });
    }

    private static final CopyOnWriteList providers = new CopyOnWriteList();

    private static Map averages = new HashMap();

    private static boolean enable_averages;
    private static Timer average_timer;

    private static CopyOnWriteList provider_listeners = new CopyOnWriteList();
    private static CopyOnWriteList derived_generators = new CopyOnWriteList();

    public static void addStatsDefinitions(String[][] stats) {
        for (int i = 0; i < stats.length; i++) {

            String name = stats[i][0];

            stats_names.add(name);

            stats_types.put(name, stats[i][1]);
        }
    }

    public static Map getStats(Set types) {
        Set expanded = new HashSet();

        Iterator it = types.iterator();

        while (it.hasNext()) {

            String type = (String) it.next();

            if (type.endsWith(".average")) {

                type = type.substring(0, type.length() - 8);
            }

            if (!type.endsWith("*")) {

                type = type + ".*";
            }

            Pattern pattern = Pattern.compile(type);

            for (int i = 0; i < stats_names.size(); i++) {

                String s = (String) stats_names.get(i);

                if (pattern.matcher(s).matches()) {

                    expanded.add(s);
                }
            }

            Iterator provider_it = providers.iterator();

            while (provider_it.hasNext()) {

                Object[] provider_entry = (Object[]) provider_it.next();

                Set provider_types = (Set) provider_entry[0];

                Iterator pt_it = provider_types.iterator();

                while (pt_it.hasNext()) {

                    String s = (String) pt_it.next();

                    if (pattern.matcher(s).matches()) {

                        expanded.add(s);
                    }
                }
            }

            Iterator derived_it = derived_generators.iterator();

            while (derived_it.hasNext()) {

                try {

                    ((derivedStatsGenerator) derived_it.next()).match(pattern, expanded);

                } catch (Throwable e) {

                    Debug.printStackTrace(e);
                }
            }
        }

        Map result = getStatsSupport(expanded);

        Map ave = averages;

        if (ave != null) {

            it = result.keySet().iterator();

            Map ave_results = new HashMap();

            while (it.hasNext()) {

                String key = (String) it.next();

                Object[] a_entry = (Object[]) ave.get(key);

                if (a_entry != null) {

                    Average average = (Average) a_entry[0];

                    ave_results.put(key + ".average", new Long((long) average.getAverage()));
                }
            }

            result.putAll(ave_results);
        }

        Iterator derived_it = derived_generators.iterator();

        while (derived_it.hasNext()) {

            try {

                ((derivedStatsGenerator) derived_it.next()).generate(result);

            } catch (Throwable e) {

                Debug.printStackTrace(e);
            }
        }

        return (result);
    }

    protected static Map getStatsSupport(Set types) {
        Map result = new HashMap();

        Iterator it = providers.iterator();

        while (it.hasNext()) {

            Object[] provider_entry = (Object[]) it.next();

            Map provider_result = new HashMap();

            Set target_types;

            if (types == null) {

                target_types = (Set) provider_entry[0];
            } else {

                target_types = types;
            }

            try {
                ((AzureusCoreStatsProvider) provider_entry[1]).updateStats(target_types, provider_result);

                Iterator pit = provider_result.entrySet().iterator();

                while (pit.hasNext()) {

                    Map.Entry pe = (Map.Entry) pit.next();

                    String key = (String) pe.getKey();
                    Object obj = pe.getValue();

                    if (obj instanceof Long) {

                        Long old = (Long) result.get(key);

                        if (old == null) {

                            result.put(key, obj);

                        } else {

                            long v = ((Long) obj).longValue();

                            result.put(key, new Long(v + old.longValue()));
                        }
                    } else {

                        result.put(key, obj);
                    }
                }
            } catch (Throwable e) {

                Debug.printStackTrace(e);
            }
        }

        return (result);
    }

    public static void registerProvider(Set types, AzureusCoreStatsProvider provider) {
        synchronized (providers) {

            providers.add(new Object[] { types, provider });
        }

        fireProvidersChangeListeners();
    }

    public static void addProvidersChangeListener(providersChangeListener l) {
        provider_listeners.add(l);
    }

    protected static void fireProvidersChangeListeners() {
        Iterator it = provider_listeners.iterator();

        while (it.hasNext()) {

            try {
                ((providersChangeListener) it.next()).providersChanged();

            } catch (Throwable e) {

                Debug.printStackTrace(e);
            }
        }
    }

    public static void registerDerivedStatsGenerator(derivedStatsGenerator gen) {
        derived_generators.add(gen);
    }

    public static synchronized void setEnableAverages(boolean enabled) {
        if (enabled == enable_averages) {

            return;
        }

        enable_averages = enabled;

        if (enabled) {

            if (average_timer == null) {

                average_timer = new Timer("AzureusCoreStats:average");

                averages = new HashMap();

                average_timer.addPeriodicEvent(AVERAGE_PERIOD, new TimerEventPerformer() {
                    private Map ave = averages;

                    public void perform(TimerEvent event) {
                        Map stats = getStatsSupport(null);

                        Iterator it = stats.entrySet().iterator();

                        boolean new_averages = false;

                        while (it.hasNext()) {

                            Map.Entry entry = (Map.Entry) it.next();

                            String key = (String) entry.getKey();
                            Object value = entry.getValue();

                            if (value instanceof Long) {

                                long last_value;
                                Average a;
                                boolean new_average;

                                Object[] a_entry = (Object[]) ave.get(key);

                                if (a_entry == null) {

                                    a = AverageFactory.MovingImmediateAverage(10);
                                    last_value = 0;

                                    a_entry = new Object[] { a, value };

                                    ave.put(key, a_entry);

                                    new_averages = new_average = true;

                                } else {
                                    a = (Average) a_entry[0];
                                    last_value = ((Long) a_entry[1]).longValue();

                                    new_average = false;
                                }

                                if (stats_types.get(key) == CUMULATIVE) {

                                    // skip initial value as 'last_value' is invalid

                                    if (!new_average) {

                                        a.update(((Long) value).longValue() - last_value);
                                    }
                                } else {

                                    a.update(((Long) value).longValue());

                                }

                                a_entry[1] = value;
                            }
                        }

                        if (new_averages) {

                            fireProvidersChangeListeners();
                        }
                    }
                });
            }
        } else {

            if (average_timer != null) {

                average_timer.destroy();

                average_timer = null;

                averages = null;
            }
        }
    }

    public static synchronized boolean getEnableAverages() {
        return (enable_averages);
    }

    public interface providersChangeListener {
        public void providersChanged();
    }

    public interface derivedStatsGenerator {
        public void match(Pattern p, Set required);

        public void generate(Map map);
    }
}
